/*

Quicksand 1.2.1

Reorder and filter items with a nice shuffling animation.

Copyright (c) 2010 Jacek Galanciak (razorjack.net) and agilope.com
Big thanks for Piotr Petrus (riddle.pl) for deep code review and wonderful docs & demos.

Dual licensed under the MIT and GPL version 2 licenses.
http://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt
http://github.com/jquery/jquery/blob/master/GPL-LICENSE.txt

Project site: http://razorjack.net/quicksand
Github site: http://github.com/razorjack/quicksand

*/

(function (jQuery) {
    jQuery.fn.quicksand = function (collection, customOptions) {     
        var options = {
            duration: 750,
            easing: 'swing',
            attribute: 'data-id', // attribute to recognize same items within source and dest
            adjustHeight: 'auto', // 'dynamic' animates height during shuffling (slow), 'auto' adjusts it before or after the animation, false leaves height constant
            useScaling: true, // disable it if you're not using scaling effect or want to improve performance
            enhancement: function(c) {}, // Visual enhacement (eg. font replacement) function for cloned elements
            selector: '> *'
        };
        jQuery.extend(options, customOptions);
        
        if (jQuery.browser.msie || (typeof(jQuery.fn.scale) == 'undefined')) {
            // Got IE and want scaling effect? Kiss my ass.
            options.useScaling = false;
        }
        
        var callbackFunction;
        if (typeof(arguments[1]) == 'function') {
            var callbackFunction = arguments[1];
        } else if (typeof(arguments[2] == 'function')) {
            var callbackFunction = arguments[2];
        }
    
        
        return this.each(function (i) {
            var val;
            var animationQueue = []; // used to store all the animation params before starting the animation; solves initial animation slowdowns
            var jQuerycollection = jQuery(collection).clone(); // destination (target) collection
            var jQuerysourceParent = jQuery(this); // source, the visible container of source collection
            var sourceHeight = jQuery(this).css('height'); // used to keep height and document flow during the animation
            
            var destHeight;
            var adjustHeightOnCallback = false;
            
            var offset = jQuery(jQuerysourceParent).offset(); // offset of visible container, used in animation calculations
            var offsets = []; // coordinates of every source collection item            
            
            var jQuerysource = jQuery(this).find(options.selector); // source collection items
            
            // Replace the collection and quit if IE6
            if (jQuery.browser.msie && jQuery.browser.version.substr(0,1)<7) {
                jQuerysourceParent.html('').append(jQuerycollection);
                return;
            }

            // Gets called when any animation is finished
            var postCallbackPerformed = 0; // prevents the function from being called more than one time
            var postCallback = function () {
                if (!postCallbackPerformed) {
                    jQuerysourceParent.html(jQuerydest.html()); // put target HTML into visible source container        
                    if (typeof callbackFunction == 'function') {
                        callbackFunction.call(this);
                    }
                    if (adjustHeightOnCallback) {
                        jQuerysourceParent.css('height', destHeight);
                    }
                    options.enhancement(jQuerysourceParent); // Perform custom visual enhancements on a newly replaced collection
                    postCallbackPerformed = 1;
                }
            };
            
            // Position: relative situations
            var jQuerycorrectionParent = jQuerysourceParent.offsetParent();
            var correctionOffset = jQuerycorrectionParent.offset();
            if (jQuerycorrectionParent.css('position') == 'relative') {
                if (jQuerycorrectionParent.get(0).nodeName.toLowerCase() == 'body') {

                } else {
                    correctionOffset.top += parseFloat(jQuerycorrectionParent.css('border-top-width'));
                    correctionOffset.left += parseFloat(jQuerycorrectionParent.css('border-left-width'));
                }
            } else {
                correctionOffset.top -= parseFloat(jQuerycorrectionParent.css('border-top-width'));
                correctionOffset.left -= parseFloat(jQuerycorrectionParent.css('border-left-width'));
                correctionOffset.top -= parseFloat(jQuerycorrectionParent.css('margin-top'));
                correctionOffset.left -= parseFloat(jQuerycorrectionParent.css('margin-left'));
            }


            // keeps nodes after source container, holding their position
            jQuerysourceParent.css('height', jQuery(this).height());
            
            // get positions of source collections
            jQuerysource.each(function (i) {
                offsets[i] = jQuery(this).offset();
            });
            
            // stops previous animations on source container
            jQuery(this).stop(); 
            jQuerysource.each(function (i) {
                jQuery(this).stop(); // stop animation of collection items
                var rawObj = jQuery(this).get(0);

                rawObj.style.position = 'absolute';
                rawObj.style.margin = '0';
                rawObj.style.top = (offsets[i].top - parseFloat(rawObj.style.marginTop) - correctionOffset.top) + 'px';
                rawObj.style.left = (offsets[i].left - parseFloat(rawObj.style.marginLeft) - correctionOffset.left) + 'px';
            });
                    
            // create temporary container with destination collection
            var jQuerydest = jQuery(jQuerysourceParent).clone();
            var rawDest = jQuerydest.get(0);
            rawDest.innerHTML = '';
            rawDest.setAttribute('id', '');
            rawDest.style.height = 'auto';
            rawDest.style.width = jQuerysourceParent.width() + 'px';
            jQuerydest.append(jQuerycollection);      
            // insert node into HTML
            // Note that the node is under visible source container in the exactly same position
            // The browser render all the items without showing them (opacity: 0.0)
            // No offset calculations are needed, the browser just extracts position from underlayered destination items
            // and sets animation to destination positions.
            jQuerydest.insertBefore(jQuerysourceParent);
            jQuerydest.css('opacity', 0.0);
            rawDest.style.zIndex = -1;
            
            rawDest.style.margin = '0';
            rawDest.style.position = 'absolute';
            rawDest.style.top = offset.top - correctionOffset.top + 'px';
            rawDest.style.left = offset.left - correctionOffset.left + 'px';
            
            
    
            

            if (options.adjustHeight === 'dynamic') {
                // If destination container has different height than source container
                // the height can be animated, adjusting it to destination height
                jQuerysourceParent.animate({height: jQuerydest.height()}, options.duration, options.easing);
            } else if (options.adjustHeight === 'auto') {
                destHeight = jQuerydest.height();
                if (parseFloat(sourceHeight) < parseFloat(destHeight)) {
                    // Adjust the height now so that the items don't move out of the container
                    jQuerysourceParent.css('height', destHeight);
                } else {
                    //  Adjust later, on callback
                    adjustHeightOnCallback = true;
                }
            }
                
            // Now it's time to do shuffling animation
            // First of all, we need to identify same elements within source and destination collections    
            jQuerysource.each(function (i) {
                var destElement = [];
                if (typeof(options.attribute) == 'function') {
                    
                    val = options.attribute(jQuery(this));
                    jQuerycollection.each(function() {
                        if (options.attribute(this) == val) {
                            destElement = jQuery(this);
                            return false;
                        }
                    });
                } else {
                    destElement = jQuerycollection.filter('[' + options.attribute + '=' + jQuery(this).attr(options.attribute) + ']');
                }
                if (destElement.length) {
                    // The item is both in source and destination collections
                    // It it's under different position, let's move it
                    if (!options.useScaling) {
                        animationQueue.push(
                                            {
                                                element: jQuery(this), 
                                                animation: 
                                                    {top: destElement.offset().top - correctionOffset.top, 
                                                     left: destElement.offset().left - correctionOffset.left, 
                                                     opacity: 1.0
                                                    }
                                            });

                    } else {
                        animationQueue.push({
                                            element: jQuery(this), 
                                            animation: {top: destElement.offset().top - correctionOffset.top, 
                                                        left: destElement.offset().left - correctionOffset.left, 
                                                        opacity: 1.0, 
                                                        scale: '1.0'
                                                       }
                                            });

                    }
                } else {
                    // The item from source collection is not present in destination collections
                    // Let's remove it
                    if (!options.useScaling) {
                        animationQueue.push({element: jQuery(this), 
                                             animation: {opacity: '0.0'}});
                    } else {
                        animationQueue.push({element: jQuery(this), animation: {opacity: '0.0', 
                                         scale: '0.0'}});
                    }
                }
            });
            
            jQuerycollection.each(function (i) {
                // Grab all items from target collection not present in visible source collection
                
                var sourceElement = [];
                var destElement = [];
                if (typeof(options.attribute) == 'function') {
                    val = options.attribute(jQuery(this));
                    jQuerysource.each(function() {
                        if (options.attribute(this) == val) {
                            sourceElement = jQuery(this);
                            return false;
                        }
                    });                 

                    jQuerycollection.each(function() {
                        if (options.attribute(this) == val) {
                            destElement = jQuery(this);
                            return false;
                        }
                    });
                } else {
                    sourceElement = jQuerysource.filter('[' + options.attribute + '=' + jQuery(this).attr(options.attribute) + ']');
                    destElement = jQuerycollection.filter('[' + options.attribute + '=' + jQuery(this).attr(options.attribute) + ']');
                }
                
                var animationOptions;
                if (sourceElement.length === 0) {
                    // No such element in source collection...
                    if (!options.useScaling) {
                        animationOptions = {
                            opacity: '1.0'
                        };
                    } else {
                        animationOptions = {
                            opacity: '1.0',
                            scale: '1.0'
                        };
                    }
                    // Let's create it
                    d = destElement.clone();
                    var rawDestElement = d.get(0);
                    rawDestElement.style.position = 'absolute';
                    rawDestElement.style.margin = '0';
                    rawDestElement.style.top = destElement.offset().top - correctionOffset.top + 'px';
                    rawDestElement.style.left = destElement.offset().left - correctionOffset.left + 'px';
                    d.css('opacity', 0.0); // IE
                    if (options.useScaling) {
                        d.css('transform', 'scale(0.0)');
                    }
                    d.appendTo(jQuerysourceParent);
                    
                    animationQueue.push({element: jQuery(d), 
                                         animation: animationOptions});
                }
            });
            
            jQuerydest.remove();
            options.enhancement(jQuerysourceParent); // Perform custom visual enhancements during the animation
            for (i = 0; i < animationQueue.length; i++) {
                animationQueue[i].element.animate(animationQueue[i].animation, options.duration, options.easing, postCallback);
            }
        });
    };
})(jQuery);